package com.express.gateway.spi.check.impl;

import com.express.gateway.common.CommonCons;
import com.express.gateway.common.GatewayAttribute;
import com.express.gateway.manage.entity.Api;
import com.express.gateway.manage.entity.ApiLimit;
import com.express.gateway.spi.check.LimitCheckHandler;
import io.vertx.ext.web.RoutingContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;

/**
 * Api的访问限制的实现
 *
 * @author flower
 */
public class LimitCheckHandlerImpl implements LimitCheckHandler {
    private static final Logger LOG = LogManager.getLogger(LimitCheckHandlerImpl.class);
    // API的相关配置
    private final Api api;
    // 流量限制
    private ApiLimit limit;

    public LimitCheckHandlerImpl(Api api) {
        super();
        this.api = api;
        if (Objects.nonNull(api.getLimitUnit())) {
            return;
        }
        if (api.getApiLimit() <= -1 && api.getIpLimit() <= -1) {
            api.setLimitUnit(null);
        }
    }

    @Override
    public void handle(RoutingContext rct) {
        if (Objects.isNull(api.getLimitUnit())) {
            rct.next();
            return;
        }
        if (Objects.nonNull(limit)) {
            // 处理上限
            if (ipsLimit(rct)) return;
            if (apiLimit(rct)) return;
            rct.next();
            return;
        }
        // 如果limit 为空则初始化一个
        this.limit = new ApiLimit(api.getIpLimit(), api.getApiLimit());
        this.limit.setCurPoint(1);
        if (api.getIpLimit() != -1) {
            String host = rct.request().remoteAddress().host();
            this.limit.addUserIpCurPotints(host, 1L);
        }
        rct.next();
    }

    private boolean apiLimit(RoutingContext rct) {
        if (limit.getApiTop() <= -1) {
            return false;
        }
        if (limit.getCurPoint() < limit.getApiTop()) {
            limit.setCurPoint(limit.getCurPoint() + 1);
            rct.next();
            return true;
        }
        if (calcLimitOver(rct)) {
            LOG.warn("接口:{}, 被调用次数已达到上限:{}", api.getPath(), limit.getApiTop());
            return true;
        }

        ApiLimit newLimit = new ApiLimit(api.getIpLimit(), api.getApiLimit());
        newLimit.setCurPoint(1);
        if (api.getIpLimit() != -1) {
            String host = rct.request().remoteAddress().host();
            newLimit.addUserIpCurPotints(host, 1L);
        }
        limit = newLimit;
        rct.next();
        return true;

    }

    private boolean ipsLimit(RoutingContext rct) {
        if (limit.getIpTop() <= -1) {
            return false;
        }
        Map<String, Long> ipPoints = limit.getUserIpCurPoints();
        String host = rct.request().remoteAddress().host();
        ipPoints.putIfAbsent(host, 1L);
        if (ipPoints.get(host) < limit.getIpTop()) {
            ipPoints.put(host, ipPoints.get(host) + 1);
            limit.setCurPoint(limit.getCurPoint() + 1);
            rct.next();
            return true;
        }
        if (calcLimitOver(rct)) {
            LOG.warn("接口:{}, 用户:{}, 请求次数已达到上限:{}", api.getPath(), limit.getIpTop(), host);
            return true;
        }

        ApiLimit newLimit = new ApiLimit(api.getIpLimit(), api.getApiLimit());
        newLimit.setCurPoint(1);
        newLimit.addUserIpCurPotints(host, 1L);
        limit = newLimit;
        rct.next();
        return true;
    }


    private boolean calcLimitOver(RoutingContext rct) {
        Instant oldTime = limit.getTimePoints().plusSeconds(api.getLimitUnit().getValue());
        Duration between = Duration.between(Instant.now(), oldTime);
        if (between.getSeconds() <= 0) {
            return false;
        }
        if (rct.response().ended()) {
            return true;
        }
        rct.response().putHeader(CommonCons.SERVER, GatewayAttribute.FULL_NAME)
                .putHeader(CommonCons.CONTENT_TYPE, api.getContentType()).setStatusCode(api.getResult().getLimitStatus())
                .end(api.getResult().getLimitExample());
        return true;
    }
}
